package com.jsjvm.client.execution;

import java.util.HashMap;
import java.util.LinkedList;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.jsjvm.TestClassLoader;
import com.jsjvm.client.RemoteClassLoading;
import com.jsjvm.client.RemoteClassLoadingAsync;
import com.jsjvm.client.classparser.JClass;
import com.jsjvm.client.classparser.jconstants.JClassConstant;
import com.jsjvm.client.util.DataInputStream;

/**
 * Lazy asynchronous class loader
 * 
 * @author ctalau
 * 
 */
public abstract class JClassLoader {
	// global cache for loaded classes
	protected HashMap<String, JClass> classes = new HashMap<String, JClass>();

	/**
	 * If the class is not cached, throw an exception to determine caller to
	 * lazily load class, and retry after it has been loaded.
	 */
	public JClass getClassByName(String name) throws JClassNotLoadedException {
		JClass ret = classes.get(name);
		if (ret == null)
			throw new JClassNotLoadedException(name);
		return ret;
	}

	/**
	 * If the class is cached in the global cache, cache it also in the
	 * JClassConstant object.
	 */
	public JClass getClassByConstant(JClassConstant jcc)
			throws JClassNotLoadedException {
		if (jcc.getJClass() == null) {
			JClass jc = getClassByName(jcc.getName());
			jcc.setJClass(jc);
		}
		return jcc.getJClass();
	}

	// The onClassLoaded method is called with the list of PREPARED classes
	public interface ClassLoadedListener {
		void onClassLoaded(LinkedList<JClass> loadedClasses);
	}

	public abstract void loadClassAndResume(String name,
			final ClassLoadedListener cll);

	/**
	 * Methods to deal with system classes.
	 * 
	 */
	// System classes - that are loaded by every non-trivial program
	public static final String OBJECT_CLASS_NAME = "java/lang/Object";
	public static final String STRING_CLASS_NAME = "java/lang/String";
	public static final String SYSTEM_CLASS_NAME = "java/lang/System";
	public static final String MATH_CLASS_NAME = "java/lang/Math";
	public static final String DOUBLE_CLASS_NAME = "java/lang/Double";
	public static final String FLOAT_CLASS_NAME = "java/lang/Float";
	public static final String PSTREAM_CLASS_NAME = "java/io/PrintStream";

	private static String[] systemClassNames = { OBJECT_CLASS_NAME,
			STRING_CLASS_NAME, SYSTEM_CLASS_NAME, MATH_CLASS_NAME,
			DOUBLE_CLASS_NAME, FLOAT_CLASS_NAME, PSTREAM_CLASS_NAME, };
	private static int nextLoading = 0;

	/**
	 * This method should be called prior to anything and given a continuation
	 */
	public void loadSystemClasses(final ClassLoadedListener cll) {

		if (nextLoading == systemClassNames.length) {
			LinkedList<JClass> loadedClasses = new LinkedList<JClass>();
			for (String name : systemClassNames)
				loadedClasses.add(this.classes.get(name));

			cll.onClassLoaded(loadedClasses);
			return;
		}

		loadClassAndResume(systemClassNames[nextLoading],
				new ClassLoadedListener() {

					/*
					 * When a class is loaded, go to the next one.
					 */
					@Override
					public void onClassLoaded(LinkedList<JClass> classes) {
						nextLoading++;
						loadSystemClasses(cll);
					};
				});
	}

	public JClass getLoadedClassByName(String name) {
		JClass ret = null;
		try {
			ret = getClassByName(name);
		} catch (JClassNotLoadedException e) {
			assert false; // system classes are loaded.
		}
		return ret;
	}

	public JClass getLoadedClassByConstant(JClassConstant jcc) {
		JClass ret = null;
		try {
			ret = getClassByConstant(jcc);
		} catch (JClassNotLoadedException e) {
			assert false; // system classes are loaded.
		}
		return ret;
	}

	/**
	 * Prepare a JClass for use.
	 */
	protected void initClass(JClass jc) {
		ExecutionEngine exec = ExecutionEngine.getInstance();
		if (jc.getClinit() != null)
			exec.call(jc.getClinit(), jc);
	}

	public static class GWTClassLoader extends JClassLoader {
		private final RemoteClassLoadingAsync remoteClassLoadingService = GWT
				.create(RemoteClassLoading.class);

		public void loadClassAndResume(String name,
				final ClassLoadedListener cont) {

			AsyncCallback<byte[]> classLoadedCB = new AsyncCallback<byte[]>() {
				public void onFailure(Throwable caught) {
					System.out.println(caught.getMessage());
				}

				/**
				 * On success resume execution
				 */
				public void onSuccess(byte[] result) {
					DataInputStream dis = new DataInputStream(result);
					final JClass jc = new JClass(dis);
					classes.put(jc.getName(), jc);
					initClass(jc);

					ClassLoadedListener newcont = new ClassLoadedListener() {
						@Override
						public void onClassLoaded(LinkedList<JClass> loadedClasses) {
							loadedClasses.add(jc);
							cont.onClassLoaded(loadedClasses);
						}
					};

					/*
					 * After the class was initialized, we proceed to its
					 * superclass.
					 * 
					 * Although the order of initialization is from descendant
					 * to ancestor, the order of invocation of <clinit>'s is the
					 * other way around because the stack is FIFO.
					 */
					try {
						jc.forceSuperClass();
					} catch (JClassNotLoadedException e) {
						
						loadClassAndResume(e.className, newcont);
						return;
					}

					newcont.onClassLoaded(new LinkedList<JClass>());
				}
			};
			remoteClassLoadingService.getClassFile(name, classLoadedCB);
		}
	}

	@SuppressWarnings("serial")
	public static class JClassNotLoadedException extends Exception {
		public String className;

		public JClassNotLoadedException(String name) {
			this.className = name;
		}
	}

	/**
	 * Singleton instance of the class loader
	 */
	private static JClassLoader inst = null;

	public static JClassLoader getInstance() {
		if (inst == null) {
			if (GWT.isClient()) {
				inst = new GWTClassLoader();
			} else {
				inst = new TestClassLoader(); // XXX just for testing
			}
		}
		return inst;
	}
}
